package at.ac.tuwien.ame

import at.ac.tuwien.ame.EcoreModelProjectPredicate
import javax.inject.Inject
import ftldsl.Assignment
import ftldsl.ComparisonOperator
import ftldsl.ConstructorFunction
import ftldsl.Expression
import ftldsl.For
import ftldsl.Function
import ftldsl.FunctionCall
import ftldsl.If
import ftldsl.ReturnStatement
import ftldsl.Transformation
import ftldsl.Variable
import org.eclipse.emf.codegen.util.ImportManager
import org.eclipse.emf.common.util.URI
import org.eclipse.emf.ecore.EObject
import org.eclipse.emf.ecore.resource.Resource
import org.eclipse.xtext.generator.IFileSystemAccess
import org.eclipse.xtext.generator.IGenerator
import org.eclipse.xtext.parsetree.reconstr.Serializer
import org.eclipse.core.internal.resources.Folder
import org.eclipse.core.resources.IProject
import org.eclipse.core.resources.IResource
import org.eclipse.core.resources.ResourcesPlugin

import static extension at.ac.tuwien.ame.FTLCompiler.*
import static extension at.ac.tuwien.ame.FTLUtils.*
import static extension com.google.common.collect.Iterables.*

class FTLGenerator implements IGenerator {
	@Inject
	ImportManager importManager
	@Inject
	FTLCodeGenerator codeGenerator

	def str(Transformation t) {
		getUnqualifiedName(t.context.name) + "Transformation"
	}

	/**
	 * Performs a lookup of the metamodel to use located within the workspace and loads it.
	 */
	def loadMetaModel(Resource input) {
		val workspace = ResourcesPlugin::workspace
		val root = workspace.root

		for (IProject p : root.projects.filter(new EcoreModelProjectPredicate)) {
			val folder = p.getFolder("model") as Folder
			for (IResource r : folder.members) {
				if (r.name.endsWith(".ecore")) {
					val path = (r as org.eclipse.core.internal.resources.File).fullPath.toFile
					resource = input.resourceSet.getResource(URI::createPlatformResourceURI(path.path, true), true)
					codeGenerator.modelName = r.name.substring(r.name.lastIndexOf('/') + 1, r.name.lastIndexOf('.'))
					return
				}
			}
		}
	}

	/**
	 * Uses an {@link FTLCodeGenerator} to compile FTL {@link Transformation}s to the desired target language.
	 */
	override doGenerate(Resource input, IFileSystemAccess fsa) {
		input.loadMetaModel

		importManager.imports.clear
		importManager.addImport("java.util.*")
		importManager.addImport("java.lang.reflect.*");
		importManager.addImport("org.eclipse.emf.common.util.*")
		importManager.addImport("org.eclipse.emf.ecore.*");
		importManager.addImport("org.eclipse.emf.ecore.resource.*");
		importManager.addImport("org.eclipse.emf.ecore.resource.impl.*");
		importManager.addImport("org.eclipse.emf.ecore.xmi.impl.*");

		for (t : input.contents.filter(typeof(Transformation))) {
			val fileName = String::format("%s/%s.java", getPackage(t.context.name).replace('.', '/'), t.str)
			fsa.generateFile(fileName, codeGenerator.generateTransformation(t))
		}
		importManager.imports.clear
	}
}

/**
 * Interface that should be implemented by FTL code generators.
 */
abstract class FTLCodeGenerator {
	@Inject
	protected Serializer serializer
	public String modelName

	/**
	 * Translates the given {@link Transformation} into the target language.
	 */
	def CharSequence generateTransformation(Transformation t)
}

/**
 * Compiles FTL to idiomatic Java source code.
 */
class JavaCodeGenerator extends FTLCodeGenerator {
	@Inject
	protected ImportManager importManager

	/**
	 * Sets or adds a new variable within the given scope.
	 */
	def V_SET(String name, EObject scope, Expression e) {
		V_SET(name, scope, e.generateExpression);
	}

	/**
	 * Attempts to resolve the return type of the given expression.
	 */
	def resolveReturnType(String returnType) {
		if (returnType == null || returnType.equals("Nothing")) "void"
		else importManager.getImportedName(returnType, true)
	}

	/**
	 * Attempts to resolve the return type of the given expression.
	 */
	def resolveReturnType(Expression e) {
		if (e == null) {
			resolveReturnType("Nothing")
		} else if (e.value != null) {
			val clazz = inferType(e.value.toString)

			if (clazz == null) {
				if (e.value.toString.startsWith("\"") && e.value.toString.endsWith("\"")) {
					return "java.lang.String"
				} else {
					try {
						Integer::parseInt(e.value.toString)
						return "java.lang.Integer"
					} catch (Exception ex) {
					}
				}

				throw new IllegalArgumentException('''Cannot determine type for expression «e.value»''')
			}

			clazz.toString
		} else if (e.invocation != null) {
			resolveReturnType(e.invocation.callee.returnType)
		}
	}

	/**
	 * Generates the code for the given {@link Transformation}. 
	 */
	override generateTransformation(Transformation t) {
		V_SET("instance", t, t.context)
		val packageName = getPackage(t.context.name)
		val code = new StringBuilder('''
				/**
				 * GENERATED BY FTL
				 */
				package «packageName»;
				''');

				importManager.markImportLocation(code)

		code.append('''

				/**
				 * Contains transformations for {@link «t.context.name.unqualifiedName»}.
				 */
				public class «t.context.name.unqualifiedName»Transformation {
					static «importManager.getImportedName(t.context.name, true)» instance;

					public static void main(String... args) throws Exception {
						if (args.length == 0) {
							System.out.println("Usage: " + «t.context.name.unqualifiedName»Transformation.class.getSimpleName() + " MODEL");
						}

						«t.context.name.unqualifiedName»Package.eINSTANCE.eClass();
						Resource.Factory.Registry reg = Resource.Factory.Registry.INSTANCE;
						Map<String, Object> map = reg.getExtensionToFactoryMap();
						map.put("xmi", new XMIResourceFactoryImpl());

						ResourceSet resSet = new ResourceSetImpl();
						Resource resource = resSet.getResource(URI.createURI(args[0]), true);

						«t.context.name»Transformation transformation = new «t.context.name»Transformation();
						List<Method> methods = new ArrayList<Method>();
						for (Method method : transformation.getClass().getMethods()) {
							if (method.getParameterTypes().length == 1 && method.getParameterTypes()[0] == «t.context.name.unqualifiedName».class) {
								methods.add(method);
							}
						}

						for (EObject obj : resource.getContents()) {
							if (obj instanceof «t.context.name.unqualifiedName») {
								for (Method method : methods) {
									method.invoke(transformation, obj);
								}
							}
						}
					}

					«FOR f : t.functions SEPARATOR "\n"»
						«f.generateFunction»
					«ENDFOR»
				}
				''')
		V_EVICT(null)
		importManager.emitSortedImports
		code.toString
	}

	/**
	 * Generates the code for the given {@link Function}. 
	 */
	def dispatch generateFunction(Function f) {
		V_SET(f.input.name, f, f.input.element)
		val returnType = f.returnType.resolveReturnType
		val code = '''
			public «importManager.getImportedName(returnType, true)» «f.name»(«importManager.getImportedName(f.input.element.name, true)» «f.input.name») {
				«IF f.guard != null»
				if (!(«serializer.serialize(f.guard)»)) {
					return«IF !"void".equals(returnType)» null«ENDIF»;
				}
				«ENDIF»
				«IF f.input.element.name == (f.eContainer as Transformation).context.name»
				«(f.eContainer as Transformation).context.name»Transformation.instance = «f.input.name»;

				«ENDIF»
				«FOR s : f.statements»
				«s.generateStatement»«IF s instanceof FunctionCall»;«ENDIF»
				«ENDFOR»
			}'''
		V_EVICT(f)
		code
	}

	/**
	 * Generates the code for the given {@link ConstructorFunction}. 
	 */
	def dispatch generateFunction(ConstructorFunction c) {
		V_SET(c.input.name, c, c.input.element)
		val type = c.returnType.resolveReturnType
		importManager.addImport("org.eclipse.emf.ecore.*")
		val code = '''
			public «type» «c.name»(«importManager.getImportedName(c.input.element.name, true)» «c.input.name») {
				«importManager.getImportedName(type, true)» «type.toFirstLower» = «modelName»Factory.eINSTANCE.create«type»();
				«FOR s : c.statements»
					«IF (s instanceof Assignment)»
					«type.toFirstLower».eSet(«modelName»Package.eINSTANCE.get«type»_«(s as Assignment).modelVariable.name.toFirstUpper»(), «(s as Assignment).assignment.generateExpression»);
					«ELSE»«s.generateStatement»«ENDIF»
				«ENDFOR»
				return «type.toFirstLower»;
			}
			'''
		V_EVICT(c)
		code
	}

	/**
	 * Generates the code for the given {@link FunctionCall}. 
	 */
	def dispatch generateStatement(FunctionCall f)
		'''«f.callee.name»(«f.parameter.name»)'''

	/**
	 * Generates the code for the given {@link ReturnStatement}. 
	 */
	def dispatch generateStatement(ReturnStatement r)
		'''return «r.returnExpression.generateExpression»;'''

	/**
	 * Generates the code for the given {@link Variable}. 
	 */
	def dispatch generateStatement(Variable v) {
		V_SET(v.name, v.eContainer, v.value)
		val value = v.value;
		if (value.coperator == ComparisonOperator::NEW) {
			val type = v.value.value.toString
			V_SET(v.name, v.eContainer, type)
			'''«importManager.getImportedName(type, true)» «v.name» = «modelName»Factory.eINSTANCE.create«type»();'''			
		} else {
			val type = if (v.returnType == null || v.returnType.equals("Nothing")) value.resolveReturnType else v.returnType
			V_SET(v.name, v.eContainer, type)
			'''«importManager.getImportedName(type, true)» «v.name» = «value.generateExpression»;'''
		}
	}

	/**
	 * Generates the code for the given {@link Assignment}. 
	 */
	def dispatch generateStatement(Assignment a) {
		if (a.operator == '+=') {
			'''«a.modelVariable.name.createGetter».add(«a.assignment.generateExpression»);'''
		} else if (a.operator == '-=') {
			'''«a.modelVariable.name.createGetter».remove(«a.assignment.generateExpression»);'''
		} else {
			if (a.assignment.coperator == ComparisonOperator::NEW) {
				val type = a.assignment.value
				'''«type» «a.modelVariable» = «modelName»Factory.eINSTANCE.create«type»();'''
			} else {
				'''«a.modelVariable.name.createSetter»(«a.assignment.generateExpression»);'''
			}
		}
	}

	/**
	 * Generates the code for the given {@link If} statement. 
	 */
	def dispatch generateStatement(If i) '''
		if («i.expression.generateExpression») {
			«FOR s : i.left SEPARATOR "\n"»
			«s.generateStatement»
			«ENDFOR»
		}«IF !i.right.nullOrEmpty» else {
			«FOR s : i.right SEPARATOR "\n"»
			«s.generateStatement»
			«ENDFOR»
		}«ENDIF»
	'''

	/**
	 * Generates the code for the given {@link For} loop. 
	 */
	def dispatch generateStatement(For f) {
		V_SET(f.iterator, f, f.collection)
		if (f.type != null) V_SET(f.iterator, f, f.type)

		val type = if (f.type != null) f.type
				else {
					var genericType = V_GET(f.collection.name)
					var resolvedType = if (genericType == null) {
						inferType(f.collection.name)
					} else {
						genericType
					}
					if (!resolvedType.endsWith(">")) resolvedType else resolvedType.substring(resolvedType.lastIndexOf('<') + 1, resolvedType.length - 1)
				}

		val code = '''
		for («importManager.getImportedName(type, true)» «f.iterator» : «createGetter(f.collection.name)») {
			«FOR s : f.body»
			«s.generateStatement»
			«ENDFOR»
		}
		'''
		V_EVICT(f)
		code
	}

	/**
	 * Generates the code for the given {@link Expression}. 
	 */
	def generateExpression(Expression e) {
		if (e == null) return ""

		val code = new StringBuilder
		if (e.value != null) {
			if (e.coperator == ComparisonOperator::NEW) {
				val type = e.value.toString
				code.append('''«modelName»Factory.eINSTANCE.create«type»()''')
			} else {
				code.append(createGetter(e.value.toString))
			}
		} else if (e.invocation != null) {
			code.append(e.invocation.generateStatement)
		}
		if (e.next != null) {
			if (e.coperator.literal != "!") {
				code.append(' ').append(e.coperator.literal).append(' ')
			} else if (e.loperator.literal != "^") {
				code.append(' ').append(e.loperator.literal).append(' ')
			}
			code.append(e.next.generateExpression as String)
		}
		code.toString
	}

	/**
	 * Generates the code for retrieving the given property or nested property.
	 */
	def createGetter(String name) {
		val parts = name.split("\\.")
		if (parts.size == 1) return name

		val variable = V_GET(parts.get(0))

		val msg = '''Variable or method «parts.get(0)» undefined or not visible in scope'''.toString
		if (variable == null)	throw new IllegalArgumentException(msg)

		val code = new StringBuilder(parts.get(0))
		for (int i : 1..(parts.size - 1)) {
			if (parts.get(i).methodCall) code.append(".").append(parts.get(i))
			else if (parts.get(i).equals("parent")) {
				val typeName = new StringBuilder(parts.get(0))
				for (int j : 1..i) {
					typeName.append(".").append(parts.get(j))
				}
				code.insert(0, '''((«typeName.toString.inferType») ''')
				code.append(".eContainer())")
			}
			else code.append(".get").append(parts.get(i).toFirstUpper).append("()")
		}
		code.toString
	}


	/**
	 * Generates the code for modifying the given property or nested property.
	 */
	def createSetter(String name) {
		val parts = name.split("\\.")
		if (parts.size == 1) return name

		val variable = V_GET(parts.get(0))

		val msg = '''Variable or method «parts.get(0)» undefined or not visible in scope'''.toString
		if (variable == null)	throw new IllegalArgumentException(msg)

		val code = new StringBuilder(parts.get(0))
		if (parts.size > 2) {
			for (int i : 1..(parts.size - 2)) {
				if (parts.get(i).methodCall) code.append(".").append(parts.get(i))
				else code.append(".get").append(parts.get(i).toFirstUpper).append("()")
			}
		}
		code.append(".set").append(parts.last.toFirstUpper)
		code.toString
	}
	
	/**
	 * Checks whether the given expression is qualified or unqualified method invocation.
	 */
	def private isMethodCall(String e) {
		e.endsWith(")")
	}
}
